(function (factory) {
    if (typeof exports === 'object' && typeof module !== "undefined") {
        module.exports = factory();
    } else if (typeof define === "function" && define.amd) {
        define([], factory);
    } else {
        var g;
        if (typeof window !== "undefined") {
            g = window;
        } else if (typeof global !== "undefined") {
            g = global;
        } else if (typeof self !== "undefined") {
            g = self;
        } else {
            g = this;
        }
        g.geoutils = factory();
    }
})(function () {

//======================================================================================================================
//region shared configurations of terrain layer
//======================================================================================================================

    // var tileImgSize = 512;
    // var basePlaneDimension = 256 * 254;
    // var extraZoom = -0.39;
    var tileImgSize = 256;
    var basePlaneDimension = 128 * 127;
    var extraZoom = 2;

    // var texImgSize = 512;
    var texImgSize = 256;

    var grandTileSize = tileImgSize / 4;

//======================================================================================================================
//endregion
//======================================================================================================================

//======================================================================================================================
//region lnglat and mercator utils
//======================================================================================================================


    var earthRadius = 6371008.8;

    /*
     * The average circumference of the world in meters.
     */
    var earthCircumfrence = 2 * Math.PI * earthRadius; // meters

    /*
     * The circumference at a line of latitude in meters.
     */
    function circumferenceAtLatitude(latitude) {
        return earthCircumfrence * Math.cos(latitude * Math.PI / 180);
    }

    function mercatorXfromLng(lng) {
        return (180 + lng) / 360;
    }

    function mercatorYfromLat(lat) {
        return (180 - (180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)))) / 360;
    }

    function mercatorZfromAltitude(altitude, lat) {
        return altitude / circumferenceAtLatitude(lat);
    }

    function lngFromMercatorX(x) {
        return x * 360 - 180;
    }

    function latFromMercatorY(y) {
        const y2 = 180 - y * 360;
        return 360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90;
    }

    function altitudeFromMercatorZ(z, y) {
        return z * circumferenceAtLatitude(latFromMercatorY(y));
    }
//======================================================================================================================
//endregion
//======================================================================================================================

//======================================================================================================================
//region tile and lnglat utils
//======================================================================================================================

    var tilebelt = (function () {

// a tile is an array [x,y,z]
        var d2r = Math.PI / 180,
            r2d = 180 / Math.PI;

        function tileToBBOX(tile) {
            var e = tile2lon(tile[0] + 1, tile[2]);
            var w = tile2lon(tile[0], tile[2]);
            var s = tile2lat(tile[1] + 1, tile[2]);
            var n = tile2lat(tile[1], tile[2]);
            return [w, s, e, n];
        }

        function tileToGeoJSON(tile) {
            var bbox = tileToBBOX(tile);
            var poly = {
                type: 'Polygon',
                coordinates:
                    [
                        [
                            [bbox[0], bbox[1]],
                            [bbox[0], bbox[3]],
                            [bbox[2], bbox[3]],
                            [bbox[2], bbox[1]],
                            [bbox[0], bbox[1]]
                        ]
                    ]
            };
            return poly;
        }

        function tile2lon(x, z) {
            return (x / Math.pow(2, z) * 360 - 180);
        }

        function tile2lat(y, z) {
            var n = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
            return (r2d * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))));
        }

        function pointToTile(lon, lat, z) {
            var tile = pointToTileFraction(lon, lat, z);
            tile[0] = Math.floor(tile[0]);
            tile[1] = Math.floor(tile[1]);
            return tile;
        }

        function getChildren(tile) {
            return [
                [tile[0] * 2, tile[1] * 2, tile[2] + 1],
                [tile[0] * 2 + 1, tile[1] * 2, tile[2] + 1],
                [tile[0] * 2 + 1, tile[1] * 2 + 1, tile[2] + 1],
                [tile[0] * 2, tile[1] * 2 + 1, tile[2] + 1],
            ];
        }

        function getParent(tile) {
            // top left
            if (tile[0] % 2 === 0 && tile[1] % 2 === 0) {
                return [tile[0] / 2, tile[1] / 2, tile[2] - 1];
            }
            // bottom left
            else if ((tile[0] % 2 === 0) && (!tile[1] % 2 === 0)) {
                return [tile[0] / 2, (tile[1] - 1) / 2, tile[2] - 1];
            }
            // top right
            else if ((!tile[0] % 2 === 0) && (tile[1] % 2 === 0)) {
                return [(tile[0] - 1) / 2, (tile[1]) / 2, tile[2] - 1];
            }
            // bottom right
            else {
                return [(tile[0] - 1) / 2, (tile[1] - 1) / 2, tile[2] - 1];
            }
        }

        function getSiblings(tile) {
            return getChildren(getParent(tile));
        }

        function hasSiblings(tile, tiles) {
            var siblings = getSiblings(tile);
            for (var i = 0; i < siblings.length; i++) {
                if (!hasTile(tiles, siblings[i])) return false;
            }
            return true;
        }

        function hasTile(tiles, tile) {
            for (var i = 0; i < tiles.length; i++) {
                if (tilesEqual(tiles[i], tile)) return true;
            }
            return false;
        }

        function tilesEqual(tile1, tile2) {
            return (
                tile1[0] === tile2[0] &&
                tile1[1] === tile2[1] &&
                tile1[2] === tile2[2]
            );
        }

        function tileToQuadkey(tile) {
            var index = '';
            for (var z = tile[2]; z > 0; z--) {
                var b = 0;
                var mask = 1 << (z - 1);
                if ((tile[0] & mask) !== 0) b++;
                if ((tile[1] & mask) !== 0) b += 2;
                index += b.toString();
            }
            return index;
        }

        function quadkeyToTile(quadkey) {
            var x = 0;
            var y = 0;
            var z = quadkey.length;

            for (var i = z; i > 0; i--) {
                var mask = 1 << (i - 1);
                switch (quadkey[z - i]) {
                    case '0':
                        break;

                    case '1':
                        x |= mask;
                        break;

                    case '2':
                        y |= mask;
                        break;

                    case '3':
                        x |= mask;
                        y |= mask;
                        break;
                }
            }
            return [x, y, z];
        }

        function bboxToTile(bboxCoords) {
            var min = pointToTile(bboxCoords[0], bboxCoords[1], 32);
            var max = pointToTile(bboxCoords[2], bboxCoords[3], 32);
            var bbox = [min[0], min[1], max[0], max[1]];

            var z = getBboxZoom(bbox);
            if (z === 0) return [0, 0, 0];
            var x = bbox[0] >>> (32 - z);
            var y = bbox[1] >>> (32 - z);
            return [x, y, z];
        }

        function getBboxZoom(bbox) {
            var MAX_ZOOM = 28;
            for (var z = 0; z < MAX_ZOOM; z++) {
                var mask = 1 << (32 - (z + 1));
                if (((bbox[0] & mask) != (bbox[2] & mask)) ||
                    ((bbox[1] & mask) != (bbox[3] & mask))) {
                    return z;
                }
            }

            return MAX_ZOOM;
        }

        function pointToTileFraction(lon, lat, z) {
            var sin = Math.sin(lat * d2r),
                z2 = Math.pow(2, z),
                x = z2 * (lon / 360 + 0.5),
                y = z2 * (0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI);
            return [x, y, z];
        }

        return {
            tile2lon: tile2lon,
            tile2lat: tile2lat,
            tileToGeoJSON: tileToGeoJSON,
            tileToBBOX: tileToBBOX,
            getChildren: getChildren,
            getParent: getParent,
            getSiblings: getSiblings,
            hasTile: hasTile,
            hasSiblings: hasSiblings,
            tilesEqual: tilesEqual,
            tileToQuadkey: tileToQuadkey,
            quadkeyToTile: quadkeyToTile,
            pointToTile: pointToTile,
            bboxToTile: bboxToTile,
            pointToTileFraction: pointToTileFraction
        };

    })();

//======================================================================================================================
//endregion
//======================================================================================================================

//======================================================================================================================
//region tile covering
//======================================================================================================================

    var cover = (function () {

        var cover = {};

        /**
         * Given a geometry, create cells and return them in a format easily readable
         * by any software that reads GeoJSON.
         *
         * @alias geojson
         * @param {Object} geom GeoJSON geometry
         * @param {Object} limits an object with min_zoom and max_zoom properties
         * specifying the minimum and maximum level to be tiled.
         * @returns {Object} FeatureCollection of cells formatted as GeoJSON Features
         */
        cover.geojson = function (geom, limits) {
            return {
                type: 'FeatureCollection',
                features: getTiles(geom, limits).map(tileToFeature)
            };
        };

        function tileToFeature(t) {
            return {
                type: 'Feature',
                geometry: tilebelt.tileToGeoJSON(t),
                properties: {}
            };
        }

        /**
         * Given a geometry, create cells and return them in their raw form,
         * as an array of cell identifiers.
         *
         * @alias tiles
         * @param {Object} geom GeoJSON geometry
         * @param {Object} limits an object with min_zoom and max_zoom properties
         * specifying the minimum and maximum level to be tiled.
         * @returns {Array<Array<number>>} An array of tiles given as [x, y, z] arrays
         */
        cover.tiles = getTiles;

        /**
         * Given a geometry, create cells and return them as
         * [quadkey](http://msdn.microsoft.com/en-us/library/bb259689.aspx) indexes.
         *
         * @alias indexes
         * @param {Object} geom GeoJSON geometry
         * @param {Object} limits an object with min_zoom and max_zoom properties
         * specifying the minimum and maximum level to be tiled.
         * @returns {Array<String>} An array of tiles given as quadkeys.
         */
        cover.indexes = function (geom, limits) {
            return getTiles(geom, limits).map(tilebelt.tileToQuadkey);
        };

        function getTiles(geom, limits) {
            var i, tile,
                coords = geom.coordinates,
                maxZoom = limits.max_zoom,
                tileHash = {},
                tiles = [];

            if (geom.type === 'Point') {
                return [tilebelt.pointToTile(coords[0], coords[1], maxZoom)];

            } else if (geom.type === 'MultiPoint') {
                for (i = 0; i < coords.length; i++) {
                    tile = tilebelt.pointToTile(coords[i][0], coords[i][1], maxZoom);
                    tileHash[toID(tile[0], tile[1], tile[2])] = true;
                }
            } else if (geom.type === 'LineString') {
                lineCover(tileHash, coords, maxZoom);

            } else if (geom.type === 'MultiLineString') {
                for (i = 0; i < coords.length; i++) {
                    lineCover(tileHash, coords[i], maxZoom);
                }
            } else if (geom.type === 'Polygon') {
                polygonCover(tileHash, tiles, coords, maxZoom);

            } else if (geom.type === 'MultiPolygon') {
                for (i = 0; i < coords.length; i++) {
                    polygonCover(tileHash, tiles, coords[i], maxZoom);
                }
            } else {
                throw new Error('Geometry type not implemented');
            }

            if (limits.min_zoom !== maxZoom) {
                // sync tile hash and tile array so that both contain the same tiles
                var len = tiles.length;
                appendHashTiles(tileHash, tiles);
                for (i = 0; i < len; i++) {
                    var t = tiles[i];
                    tileHash[toID(t[0], t[1], t[2])] = true;
                }
                return mergeTiles(tileHash, tiles, limits);
            }

            appendHashTiles(tileHash, tiles);
            return tiles;
        }

        function mergeTiles(tileHash, tiles, limits) {
            var mergedTiles = [];

            for (var z = limits.max_zoom; z > limits.min_zoom; z--) {

                var parentTileHash = {};
                var parentTiles = [];

                for (var i = 0; i < tiles.length; i++) {
                    var t = tiles[i];

                    if (t[0] % 2 === 0 && t[1] % 2 === 0) {
                        var id2 = toID(t[0] + 1, t[1], z),
                            id3 = toID(t[0], t[1] + 1, z),
                            id4 = toID(t[0] + 1, t[1] + 1, z);

                        if (tileHash[id2] && tileHash[id3] && tileHash[id4]) {
                            tileHash[toID(t[0], t[1], t[2])] = false;
                            tileHash[id2] = false;
                            tileHash[id3] = false;
                            tileHash[id4] = false;

                            var parentTile = [t[0] / 2, t[1] / 2, z - 1];

                            if (z - 1 === limits.min_zoom) mergedTiles.push(parentTile);
                            else {
                                parentTileHash[toID(t[0] / 2, t[1] / 2, z - 1)] = true;
                                parentTiles.push(parentTile);
                            }
                        }
                    }
                }

                for (i = 0; i < tiles.length; i++) {
                    t = tiles[i];
                    if (tileHash[toID(t[0], t[1], t[2])]) mergedTiles.push(t);
                }

                tileHash = parentTileHash;
                tiles = parentTiles;
            }

            return mergedTiles;
        }

        function polygonCover(tileHash, tileArray, geom, zoom) {
            var intersections = [];

            for (var i = 0; i < geom.length; i++) {
                var ring = [];
                lineCover(tileHash, geom[i], zoom, ring);

                for (var j = 0, len = ring.length, k = len - 1; j < len; k = j++) {
                    var m = (j + 1) % len;
                    var y = ring[j][1];

                    // add interesction if it's not local extremum or duplicate
                    if ((y > ring[k][1] || y > ring[m][1]) && // not local minimum
                        (y < ring[k][1] || y < ring[m][1]) && // not local maximum
                        y !== ring[m][1]) intersections.push(ring[j]);
                }
            }

            intersections.sort(compareTiles); // sort by y, then x

            for (i = 0; i < intersections.length; i += 2) {
                // fill tiles between pairs of intersections
                y = intersections[i][1];
                for (var x = intersections[i][0] + 1; x < intersections[i + 1][0]; x++) {
                    var id = toID(x, y, zoom);
                    if (!tileHash[id]) {
                        tileArray.push([x, y, zoom]);
                    }
                }
            }
        }

        function compareTiles(a, b) {
            return (a[1] - b[1]) || (a[0] - b[0]);
        }

        function lineCover(tileHash, coords, maxZoom, ring) {
            var prevX, prevY;

            for (var i = 0; i < coords.length - 1; i++) {
                var start = tilebelt.pointToTileFraction(coords[i][0], coords[i][1], maxZoom),
                    stop = tilebelt.pointToTileFraction(coords[i + 1][0], coords[i + 1][1], maxZoom),
                    x0 = start[0],
                    y0 = start[1],
                    x1 = stop[0],
                    y1 = stop[1],
                    dx = x1 - x0,
                    dy = y1 - y0;

                if (dy === 0 && dx === 0) continue;

                var sx = dx > 0 ? 1 : -1,
                    sy = dy > 0 ? 1 : -1,
                    x = Math.floor(x0),
                    y = Math.floor(y0),
                    tMaxX = dx === 0 ? Infinity : Math.abs(((dx > 0 ? 1 : 0) + x - x0) / dx),
                    tMaxY = dy === 0 ? Infinity : Math.abs(((dy > 0 ? 1 : 0) + y - y0) / dy),
                    tdx = Math.abs(sx / dx),
                    tdy = Math.abs(sy / dy);

                if (x !== prevX || y !== prevY) {
                    tileHash[toID(x, y, maxZoom)] = true;
                    if (ring && y !== prevY) ring.push([x, y]);
                    prevX = x;
                    prevY = y;
                }

                while (tMaxX < 1 || tMaxY < 1) {
                    if (tMaxX < tMaxY) {
                        tMaxX += tdx;
                        x += sx;
                    } else {
                        tMaxY += tdy;
                        y += sy;
                    }
                    tileHash[toID(x, y, maxZoom)] = true;
                    if (ring && y !== prevY) ring.push([x, y]);
                    prevX = x;
                    prevY = y;
                }
            }

            if (ring && y === ring[0][1]) ring.pop();
        }

        function appendHashTiles(hash, tiles) {
            var keys = Object.keys(hash);
            for (var i = 0; i < keys.length; i++) {
                tiles.push(fromID(+keys[i]));
            }
        }

        function toID(x, y, z) {
            var dim = 2 * (1 << z);
            return ((dim * y + x) * 32) + z;
        }

        function fromID(id) {
            var z = id % 32,
                dim = 2 * (1 << z),
                xy = ((id - z) / 32),
                x = xy % dim,
                y = ((xy - x) / dim) % dim;
            return [x, y, z];
        }

        return cover;

    })();

//======================================================================================================================
//endregion
//======================================================================================================================

//======================================================================================================================
//region camera and zoom
//======================================================================================================================


    function getBaseLog(base, result) {
        return Math.log(result) / Math.log(base);
    }

    function calcZoomOfCamDist(pt) {
        // var pt = this.controls.target.distanceTo(this.controls.object.position);
        return Math.min(Math.max(getBaseLog(0.5, pt / 12000) + 4, 0), 22);
    }

    function clip(v, min, max) {
        return Math.max(min, Math.min(v, max));
    }

    function calcCamDistOfZoom(zoom) {
        zoom = clip(zoom, 0, 22);
        return 12000 * Math.pow(0.5, (zoom - 4));
    }

    /*
     * 2^zoom (tile) * 256 (px/tile) => pixels at zoom
     */
    function calcPerimeterAtLat(lat) {
        return Math.cos(lat * Math.PI / 180) * 40075040;
    }

    function calcZoomOfMeterPerPixel(lat, meterPerPx) {
        var perimeter = calcPerimeterAtLat(lat); // meters
        var pixels = perimeter / meterPerPx;
        var zoom = Math.log(pixels / 256) / Math.log(2);
        return zoom;
    }

    // spherical mercator
    function getMeterPerPxAtLat(lat, tileSize, zoom) {
        return Math.abs(
            40075000 * Math.cos(lat * Math.PI / 180) / (Math.pow(2, zoom) * tileSize)
        );
    }

//======================================================================================================================
//endregion
//======================================================================================================================

//======================================================================================================================
//region geometry utils for elevation tiles
//======================================================================================================================

    function slashify(input) {
        return input.join('/');
    }

    function deslash(input) {
        return input.split('/').map(function (str) {
            return parseInt(str)
        });
    }


    var totalCount = 3 * grandTileSize * grandTileSize;
    var rowCount = 3 * grandTileSize;
    //above, left, below, right
    var neighborTiles = [
        [0, 0, -1], [0, -1, 0],
        [0, 0, 1], [0, 1, 0]
    ];
    var row = [[], [], [], []];

    //get specific pixel indices of each edge
    for (var c = 0; c < rowCount; c += 3) {
        //top, left, bottom, right
        row[0].push(c + 1);
        row[1].push(c / 3 * (rowCount) + 1);
        row[2].push(c + 1 + totalCount - rowCount);
        row[3].push((c / 3 + 1) * (rowCount) - 2);
    }

    /*
     * data: grandTileSize * grandTileSize * [x,z,y]
     * data: array
     */
    //fill seam between elevation tiles by adopting the edge of neighboring tiles
    function resolveSeams(scene, data, neighborTiles, [z, x, y]) {
        //iterate through neighbors
        neighborTiles.forEach(function (tile, index) {
            //figure out neighbor tile coordinate
            var targetTile = tile.map(function (coord, index) {
                return coord + [z, x, y][index];
            });

            //if neighbor exists,
            var neighbor = scene.getObjectByProperty('coords', slashify(targetTile));
            if (neighbor) {
                // indices that need to be overwritten
                var indicesToChange = row[index];
                //indices of neighbor vertices to copy
                var neighborIndices = row[(index + 2) % 4];
                var neighborVertices = neighbor.geometry.attributes.position.array;

                for (var a = 0; a < grandTileSize; a++) {
                    data[indicesToChange[a]] = neighborVertices[neighborIndices[a]];
                }
            }
        });
        return data;
    }

    function roundTo(number, decimals) {
        return number.toFixed(decimals) / 1;
    }

//======================================================================================================================
//endregion
//======================================================================================================================

//======================================================================================================================
//region lng and lat transformation : copied from wutil/coordUtils
//======================================================================================================================

    function AABB() {
        this.minX = Number.MAX_SAFE_INTEGER;
        this.maxX = - Number.MAX_SAFE_INTEGER;
        this.minY = Number.MAX_SAFE_INTEGER;
        this.maxY = - Number.MAX_SAFE_INTEGER;

        var self = this;

        this.combineXY = function (x, y) {
            if (self.minX > x) self.minX = x;
            if (self.maxX < x) self.maxX = x;
            if (self.minY > y) self.minY = y;
            if (self.maxY < y) self.maxY = y;
        };
    }

    Object.defineProperties(AABB.prototype, {
        cenX: {
            get: function () {
                return (this.maxX + this.minX) / 2;
            },
        },
        cenY: {
            get: function () {
                return (this.maxY + this.minY) / 2;
            },
        },
    });

    function LngLat(lng, lat) {
        if (lng && lat) {
            this.lng = lng;
            this.lat = lat;
        } else if (lng && lng[0] && lng[1]) {
            this.lng = lng[0];
            this.lat = lat[0];
        } else {
            this.lng = 0;
            this.lat = 0;
        }
    }

    var pi = 3.1415926535897932384626;
    var a = 6378245.0;
    var ee = 0.00669342162296594323;

    function outOfChina(lng, lat) {
        if (lng < 72.004 || lng > 137.8347)
            return true;
        if (lat < 0.8293 || lat > 55.8271)
            return true;
        return false;
    }

    function transform(lng, lat) {
        if (outOfChina(lng, lat)) {
            return new LngLat(lng, lat);
        }
        var dLat = transformLat(lng - 105.0, lat - 35.0);
        var dLon = transformLon(lng - 105.0, lat - 35.0);
        var radLat = lat / 180.0 * pi;
        var magic = Math.sin(radLat);
        magic = 1 - ee * magic * magic;
        var sqrtMagic = Math.sqrt(magic);
        dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
        dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
        var mgLat = lat + dLat;
        var mgLon = lng + dLon;
        return new LngLat(mgLon, mgLat);
    }

//dlng, dlat
    function transformLat(x, y) {
        var ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y
            + 0.2 * Math.sqrt(Math.abs(x));
        ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
        ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;
        return ret;
    }

//dlng, dlat
    function transformLon(x, y) {
        var ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1
            * Math.sqrt(Math.abs(x));
        ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
        ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0
            * pi)) * 2.0 / 3.0;
        return ret;
    }

    function wgs84_to_gcj02(lng, lat) {
        return transform(lng, lat);
    }

    /*
     * Since WGS84 to GCJ02 transformation is monotonic, we can use a
     * minimizing rectangle box containing the target point to recursively
     * approach the target point.
     *
     * We know the max deviation of WGS84 to GCJ02 is around 700 meters
     * so it's safe to use a initial box of 0.015 degree radius (max 0.0102)
     * https://kongxz.com/2013/10/wgs-cgj/
     */
    function gcj02_to_wgs84(gcjlng, gcjlat, precision) {
        /*
         * 1e1: 111km
         * 1e-1: 11.1km
         * 1e-2: 1.11km
         * 1e-3: 111m
         * 1e-4: 11m
         * 1e-5: 1m
         * 1e-6: 0.1m
         * 1e-7: 1cm
         * 1e-8: 1mm
         */
        precision = precision || 1e-6;

        if (gcjlng.lng && gcjlng.lat) {
            gcjlat = gcjlng.lat;
            gcjlng = gcjlng.lng;
        }

        var wgsRect = new AABB();
        var delta = 0.015;
        wgsRect.combineXY(gcjlng + delta, gcjlat + delta);
        wgsRect.combineXY(gcjlng - delta, gcjlat - delta);

        /*
         * in case it doesn't converge (theorically impossible), breaks it
         * regularly, 15 iters (1e-15) can reach the best precision of a 64bit float
         * number (javascript)
         */
        var maxIter = 20;

        while ((maxIter--) > 0) {
            var cenX = wgsRect._cenX || wgsRect.cenX;
            var cenY = wgsRect._cenY || wgsRect.cenY;
            var cen_gcj = wgs84_to_gcj02(cenX, cenY);

            /*
             * the point should be in the box, else errors happen, the nearest point
             * in the box will be returned
             */

            if (Math.abs(cen_gcj.lng - gcjlng) > precision || Math.abs(cen_gcj.lat - gcjlat) > precision) {
                if (cen_gcj.lng > gcjlng) {
                    if (cen_gcj.lat > gcjlat) {
                        // bottom left
                        wgsRect.maxX = cenX;
                        wgsRect.maxY = cenY;
                    } else {
                        // top left
                        wgsRect.maxX = cenX;
                        wgsRect.minY = cenY;
                    }
                } else {
                    if (cen_gcj.lat > gcjlat) {
                        // bottom right
                        wgsRect.minX = cenX;
                        wgsRect.maxY = cenY;
                    } else {
                        // top right
                        wgsRect.minX = cenX;
                        wgsRect.minY = cenY;
                    }
                }
                wgsRect._cenX = (wgsRect.minX + wgsRect.maxX) / 2;
                wgsRect._cenY = (wgsRect.minY + wgsRect.maxY) / 2;
            } else {
                break;
            }
        }
        return new LngLat(cenX, cenY);
    }

//======================================================================================================================
//endregion
//======================================================================================================================

//======================================================================================================================
//region bluring
//======================================================================================================================

// extend edge

    /*
     * all data stored in `buffer` in format of   [x y z ...]* (all components in a plain array)
     * an item is composed of components [x y z ...]
     * size: item count in one axis (rect)
     * step: component amount in an item
     */
    function getValueInBuffer(buffer, size, step, x, y, offset) {
        if (x < 0) x = 0;
        if (y < 0) y = 0;
        if (x >= size) x = size - 1;
        if (y >= size) y = size - 1;
        offset = offset || 0;
        return buffer[step * (size * y + x) + offset];
    }

// var boxBlurMatrix = [
//     [-1, -1], [0, -1], [1, -1],
//     [-1, 0], [0, 0], [1, 0],
//     [-1, 1], [0, 1], [1, 1],
// ];
    var boxSize = 9; // 3*3

    function boxBlurElevation(buffer, size, x, y, output) {
        var sum = 0;

        for (var i = -1; i <= 1; i++) {
            for (var j = -1; j <= 1; j++) {
                sum += getValueInBuffer(buffer, size, 1, x + i, y + j, 0);
            }
        }

        var baseOffset = (size * y + x) * 1;
        output[baseOffset + 0] = sum / boxSize;
    }

    var gaussianWeights = [1, 2, 1, 2, 4, 2, 1, 2, 1];
    var gaussianBase = 16;

    function gaussianBlur3Elevation(buffer, size, x, y, output) {
        var sum = 0;

        for (var i = -1; i < 2; i++) {
            for (var j = -1; j < 2; j++) {
                var idx = i * 3 + j + 4;
                var weight = gaussianWeights[idx];
                sum += weight * getValueInBuffer(buffer, size, 1, x + i, y + j, 0);
            }
        }

        var baseOffset = (size * y + x) * 1;
        output[baseOffset + 0] = sum / gaussianBase;
    }

    var gaussian5Weights = [1, 4, 6, 4, 1, 4, 16, 24, 16, 4, 6, 24, 36, 24, 6, 4, 16, 24, 16, 4, 1, 4, 6, 4, 1];
    var gaussian5Base = 256;

    function gaussianBlur5Elevation(buffer, size, x, y, output) {
        var sum = 0;

        for (var i = -2; i < 3; i++) {
            for (var j = -2; j < 3; j++) {
                var idx = i * 5 + j + 12;
                var weight = gaussian5Weights[idx];
                sum += weight * getValueInBuffer(buffer, size, 1, x + i, y + j, 0);
            }
        }

        var baseOffset = (size * y + x) * 1;
        output[baseOffset + 0] = sum / gaussian5Base;
    }

    function applyElevationBlur(array, size, target) {
        for (var i = 0; i < size; i++) {
            for (var j = 0; j < size; j++) {
                gaussianBlur5Elevation(array, size, j, i, target);
                // gaussianBlur3Elevation(array, size, j, i, target);
                // boxBlurElevation(array, size, j, i, target);
            }
        }
        return target;
    }

//======================================================================================================================
//endregion
//======================================================================================================================

    return {
        /*
         * tilebelt utils
         */
        tile2lon: tilebelt.tile2lon,
        tile2lat: tilebelt.tile2lat,
        pointToTile: tilebelt.pointToTile,
        /*
         * shared configuration
         */
        tileImgSize: tileImgSize,
        grandTileSize: grandTileSize,
        texImgSize: texImgSize,
        basePlaneDimension: basePlaneDimension,
        extraZoom: extraZoom,
        /*
         * tilecover utils
         */
        tiles: cover.tiles,
        getTiles: cover.getTiles,
        /*
         * camera and tile utils
         */
        calcZoomOfMeterPerPixel: calcZoomOfMeterPerPixel,
        calcCamDistOfZoom: calcCamDistOfZoom,
        getMeterPerPxAtLat: getMeterPerPxAtLat,
        /*
         * geometry utils
         */
        slashify: slashify,
        neighborTiles: neighborTiles,
        resolveSeams: resolveSeams,
        roundTo: roundTo,
        /*
         * lng lat transform
         */
        gcj02_to_wgs84: gcj02_to_wgs84,
        wgs84_to_gcj02: wgs84_to_gcj02,
        /*
         * bluring
         */
        applyElevationBlur: applyElevationBlur,
    };

});